package sk.uniba.fmph.asdf.db;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.ArrayList;

import sk.uniba.fmph.asdf.Session;
import sk.uniba.fmph.asdf.exceptions.DatabaseException;
import sk.uniba.fmph.asdf.exceptions.LoginException;
import sk.uniba.fmph.asdf.exceptions.NoSuchAccountException;
import sk.uniba.fmph.asdf.exceptions.OfflineException;
import sk.uniba.fmph.asdf.exceptions.PermissionDeniedException;
import sk.uniba.fmph.asdf.exceptions.WrongPasswordException;
import sk.uniba.fmph.asdf.utils.Hash;

/**
 * Trieda zabezpecuje spravu uzivatelskych uctov, co zahrnuje vytvaranie novych
 * uctov, upravu existujucich, mazanie uctov, ako aj samotne prihlasenie do
 * systemu.
 *
 * @author Marek Timura
 * @author Ivor Uhliarik
 */
public class AccountManager {
	/** Meno suboru, do ktoreho sa ma zapisovat has hesla pre offline ucely */
	private static final String PASSWORD_HASH_FILE = "login_info.dat";
	private static final String ACCOUNT_FILE = "account_info.dat";

	/** staticka referencia na objekt, ktora sa stara o SQL volania */
	private static SQLQueryHandler queryHandler = new SQLQueryHandler();

    private static boolean offlineMode = false;

	/**
	 * Prihlasi uzivatela. Ak prihlasenie prebehlo neuspesne, vrati vynimku.
	 *
	 * @param email
	 *            prihlasovacie meno uzivatela
	 * @param password
	 *            heslo pouzivatela
	 * @return ucet pouzivatela
	 * @throws NoSuchAccountException
	 * @throws WrongPasswordException
	 * @throws DatabaseException
	 */
	public static Account loginUser(final String email, final String password)
			throws NoSuchAccountException, WrongPasswordException,
			DatabaseException, OfflineException {
		try {
            queryHandler.openConnection();
			queryHandler.getResultSet(SQLQuery.GET_USER_BY_USERNAME, email);
		} catch (DatabaseException de) {
			throw new NoSuchAccountException(":(");
		}
		Account loggedAccount = null;
		try {
			loggedAccount =
					(Account) queryHandler.getResultSet(SQLQuery.LOGIN_USER,
							email, password);
		} catch (DatabaseException de) {
			throw new WrongPasswordException("nevies hackovat");
		}

		// Ulozime ucet a heslo pre moznost prihlasenia sa v offline rezime.
		saveAccount(loggedAccount);
		saveHashedPassword(email, password);
		return loggedAccount;
	}

	/**
	 * Prihlasi pouzivatela v offline rezime. Prihlasenie sa podari, ak sa
	 * pouzivatel do doby volania aspon raz prihlasil online. Prihlasit sa moze
	 * iba naposledy prihlaseny pouzivatel.
	 *
	 * @param email
	 *            Email uctu, s ktorym sa pouzivatel snazi prihlasit.
	 * @param password
	 *            Heslo, s ktorym sa pouzivatel snazi prihlasit.
	 * @return Ucet pouzivatela.
	 * @throws IOException
	 *             Ak sa vyskytla chyba pri citani suboru z predchadzajuceho
	 *             online prihlasenia.
	 * @throws LoginException
	 *             Ak sa nepodarilo prihlasit. Bud nesedi email alebo heslo,
	 *             alebo tento ucet este nebol prihlaseny online.
	 */
	public static Account loginOfflineUser(final String email,
			final String password) throws IOException, LoginException {
		if (verifyOfflineAccount(email, password)) {
			ObjectInputStream ois =
					new ObjectInputStream(new FileInputStream(email + '/'
							+ ACCOUNT_FILE));
			Account account;
			try {
				account = (Account) ois.readObject();
			} catch (ClassNotFoundException e) {
				e.printStackTrace();
				ois.close();
				return null;
			}
			ois.close();
			return account;
		}
		throw new LoginException("Nepodarilo sa prihlasit v offline rezime.");
	}

	/**
	 * Ulozi ucet pouzivatela do suboru.
	 *
	 * @param account
	 *            Ucet pouzivatela, ktory sa ma ulozit.
	 */
	private static void saveAccount(final Account account) {
		if (account == null) {
			return;
		}

		ObjectOutputStream oos;
		try {
			File accountDir = new File(account.getEmail());
			accountDir.mkdir();

			oos =
					new ObjectOutputStream(new FileOutputStream(
							account.getEmail() + '/' + ACCOUNT_FILE));
			oos.writeObject(account);
			oos.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	/**
	 * Ulozi SHA-1 has hesla do lokalneho suboru pre moznost offline rezimu.
	 *
	 * @param email
	 *            Email uctu, ktoreho heslo sa ma ulozit.
	 * @param password
	 *            Heslo, ktoreho has sa ma ulozit.
	 */
	private static void saveHashedPassword(final String email,
			final String password) {
		FileWriter fw = null;
		try {
			File accountDir = new File(email);
			accountDir.mkdir();

			fw = new FileWriter(email + '/' + PASSWORD_HASH_FILE);
		} catch (IOException e) {
			e.printStackTrace();
		}

		try {
			fw.write(Hash.sha1(password));
			fw.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	/**
	 * Overi, ci je mozne sa so zadanym emailom a heslom prihlasit v offline
	 * rezime, teda ci je has hesla ulozeny v subore.
	 *
	 * @param email
	 *            Email uctu, s ktorym sa pouzivatel snazi prihlasit.
	 * @param password
	 *            Heslo, s ktorym sa pouzivatel snazi prihlasit.
	 * @return True, ak je has hesla spravny.
	 */
	private static boolean verifyOfflineAccount(final String email, final String password) {
		FileInputStream fis;
		try {
			fis = new FileInputStream(email + '/' + PASSWORD_HASH_FILE);
			byte[] data = new byte[40];
			int i = 0;
			int count = 0;

			while ((count = fis.read(data, i, data.length - i)) != -1 && count != 0) {
				i += count;
			}
			String savedPassword = new String(data);
			fis.close();

			return Hash.sha1(password).equals(savedPassword);
		} catch (IOException e) {
			return false;
		}
	}

	/**
	 * Vytvori noveho uzivatela v databaze s prislusnymi parametrami.
	 *
	 * @param email
	 *            prihlasovacie meno / email uzivatela
	 * @param fullname
	 *            cele meno uzivatela
	 * @param password
	 *            heslo uzivatela
	 * @return boolovsku hodnotu, vypovedajucu o uspesnosti vytvorenia noveho
	 *         uctu
	 * @throws DatabaseException
	 * @throws OfflineException
	 */
	public static boolean createNewUser(final String email,
			final FullName fullName, final String password)
			throws DatabaseException, OfflineException {
		queryHandler.openConnection();
		return (queryHandler.executeUpdate(SQLQuery.CREATE_NEW_USER, email,
				fullName.getFirstName(), fullName.getLastName(), password) == 1);
	}

	/**
	 * Vracia zoznam uzivatelov v databaze.
	 *
	 * @return zoznam uzivatelskych utov
	 * @throws DatabaseException
	 */
	@SuppressWarnings("unchecked")
	public static ArrayList<Account> getUsers() throws DatabaseException {
		return (ArrayList<Account>) queryHandler
				.getResultSet(SQLQuery.GET_USERS);
	}

	/**
	 * Vracia uzivatela z databazy s danym id.
	 *
	 * @param id
	 *            id uzivatela
	 * @return ucet uzivatela s danym id
	 * @throws DatabaseException
	 */
	public static Account getUser(final int id) throws DatabaseException {
		return (Account) queryHandler.getResultSet(SQLQuery.GET_USER, id);
	}

	/**
	 * Metoda upravi heslo uzivatela v databaze. Administrator moze upravit
	 * ktorehokolvek uzivatela, ostatni mozu upravit iba svoje heslo.
	 *
	 * @param id
	 *            id uzivatela
	 * @param password
	 *            nove heslo
	 * @return boolovska hodnota vypovedajuca o uspesnosti zmeny hesla uzivatela
	 * @throws PermissionDeniedException
	 * @throws DatabaseException
	 */
	public static boolean editUserPassword(final int id, final String password)
			throws PermissionDeniedException, DatabaseException {
		// Ak si pouzivatel upravuje svoje heslo, povolime mu to, inac musi byt
		// administrator
		if (id == Session.getAccount().getId()
				|| Session.getAccount().isAdmin()) {
			return (queryHandler.executeUpdate(SQLQuery.EDIT_USER_PASSWORD, id,
					password) == 1);
		} else {
			throw new PermissionDeniedException();
		}
	}

	/**
	 * Metoda upravi udaje o uzivatelovi. Administratot moze menit udaje
	 * vsetkych uzivatelov, ostatni mozu menit iba svoje udaje.
	 *
	 * @param id
	 *            id uzivatela
	 * @param email
	 *            prihlasovacie meno uzivatela
	 * @param fullName
	 *            cele meno uzivatela
	 * @return boolovska hodnota vypovedajuca o uspesnosti zmeny udajov o
	 *         uzivatelovi
	 * @throws PermissionDeniedException
	 * @throws DatabaseException
	 */
	public static boolean editUserDetails(final int id, final String email,
			final FullName fullName) throws PermissionDeniedException,
			DatabaseException {
		// Ak si pouzivatel upravuje svoje udaje, povolime mu to, inac musi byt
		// administrator
		if (id == Session.getAccount().getId()
				|| Session.getAccount().isAdmin()) {
			return (queryHandler.executeUpdate(SQLQuery.EDIT_USER_DETAILS, id,
					fullName.getFirstName(), fullName.getLastName(), email) == 1);
		} else {
			throw new PermissionDeniedException();
		}
	}

	/**
	 * Metoda zmaze uzivatela s danym id v databaze.
	 *
	 * @param id
	 *            id uzivatela
	 * @return boolovska hodnota vypovedajuca o uspesnosti zmazania uzivatela s
	 *         danym id
	 * @throws PermissionDeniedException
	 * @throws DatabaseException
	 */
	public static boolean deleteUser(final int id)
			throws PermissionDeniedException, DatabaseException {
		if (!Session.getAccount().isAdmin()) {
			throw new PermissionDeniedException();
		}
		return (queryHandler.executeUpdate(SQLQuery.DELETE_USER, id) == 1);
	}
}
